﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Microsoft.CSharp;
using Microsoft.VisualBasic;

namespace NameMapping.CodeDom
{
    public class Generator
    {
        private XDocument _doc;
        private CodeCompileUnit compileUnit;
        private Dictionary<string, string> classDictionary;
        private string NameSpace { get; set; }

        private Generator(XDocument doc)
        {
            _doc = doc;
        }

        public static CodeCompileUnit Parse(XDocument doc, string nameSpace)
        {
            var generator = new Generator(doc);
            generator.NameSpace = nameSpace;

            generator.GenCompileUnit();
            return generator.compileUnit;
        }
        public static CodeCompileUnit Parse(string fileName, string nameSpace)
        {
            var proc = new PreProcessor();
            XmlDocument doc = new XmlDocument();
            var fileInfo = new FileInfo(fileName);
            using (StreamReader tr = File.OpenText(fileInfo.FullName))
            {
                doc.Load(tr.BaseStream);
            }
            XDocument xDoc = XDocument.Parse(proc.Transform(doc));
            return Parse(xDoc, nameSpace);
        }

        /*public static string CodeDomToString(CodeCompileUnit compileUnit, string providerType)
        {
            CodeDomProvider provider;
            //if (providerType == "cs") //todo: add dependency here
            {
                provider = new CSharpCodeProvider();
            }

            StringBuilder sb = new StringBuilder();
            using (TextWriter sw = new StringWriter(sb))
            {
                provider.GenerateCodeFromCompileUnit(compileUnit, sw, new CodeGeneratorOptions());
            }
            return sb.ToString();
        }/**/

        private string Gen()
        {
            GenCompileUnit();

            CodeDomProvider provider = new CSharpCodeProvider();

            StringBuilder sb = new StringBuilder();
            using (TextWriter sw = new StringWriter(sb))
            {
                provider.GenerateCodeFromCompileUnit(compileUnit, sw, new CodeGeneratorOptions());
            }
            return sb.ToString();
        }

        private void GenCompileUnit()
        {
            classDictionary = new Dictionary<string, string>();
            compileUnit = new CodeCompileUnit();
            genNamespace(NameSpace); //todo: send outside as initial file name

            genCoreClasses();
            genNmClasses();
            genAliasClasses();
            genMapClass();
        }
        private void genMapClass()
        {
            CodeTypeDeclaration class1 = new CodeTypeDeclaration("Map");
            CodeMemberProperty ct;
            ct = new CodeMemberProperty();
            ct.Name = "nameMapping";
            ct.HasGet = true;
            ct.HasSet = false;
            ct.Type = new CodeTypeReference("NameMapping");
            ct.Attributes = MemberAttributes.Public | MemberAttributes.Static;
            ct.GetStatements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(ct.Type, new CodeExpression[] { })));
            class1.Members.Add(ct);

            ct = new CodeMemberProperty();
            ct.Name = "aliases";
            ct.HasGet = true;
            ct.HasSet = false;
            ct.Type = new CodeTypeReference("Aliases");
            ct.Attributes = MemberAttributes.Public | MemberAttributes.Static;
            ct.GetStatements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(ct.Type, new CodeExpression[] { })));
            class1.Members.Add(ct);

            compileUnit.Namespaces[0].Types.Add(class1);
        }

        #region Generators
        private void genNamespace(string nsName)
        {
            CodeNamespace ns = new CodeNamespace(nsName);
            ns.Imports.Add(new CodeNamespaceImport("System"));
            ns.Imports.Add(new CodeNamespaceImport("System.Linq"));

            compileUnit.Namespaces.Add(ns);
        }
        private void genNmClasses()
        {
            genRootNameMappingClass();
            foreach (var node in _doc.Descendants("NM"))
            {
                string fullNmPath = getFullNmPath(node);
                string className = getClassNameForNmNode(node);
                
                classDictionary.Add(fullNmPath, className); //todo: maybe remove?

                CodeTypeDeclaration class1 = new CodeTypeDeclaration(className);

                class1.Comments.Add(new CodeCommentStatement("Original Guid"+node.Attribute("guid").Value));

                class1.IsClass = true;
                class1.BaseTypes.Add(new CodeTypeReference("UiMappedNode"));
                
                //constructor
                CodeConstructor constructor = new CodeConstructor();
                constructor.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                constructor.BaseConstructorArgs.Add(new CodePrimitiveExpression(fullNmPath));
                class1.Members.Add(constructor);

                //childs
                foreach (var child in node.Elements("NM"))
                {
                    CodeMemberProperty ct = new CodeMemberProperty();
                    ct.Name = child.Attribute("name").Value;
                    ct.HasGet = true;
                    ct.HasSet = false;
                    ct.Type = new CodeTypeReference(getClassNameForNmNode(child));
                    ct.Attributes = MemberAttributes.Public | MemberAttributes.Final;

                    ct.GetStatements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(ct.Type, new CodeExpression[] { })));

                    class1.Members.Add(ct);
                }

                //parent
                CodeMemberMethod getParentMethod = new CodeMemberMethod();
                getParentMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                getParentMethod.Name = "getParent";
                getParentMethod.ReturnType = new CodeTypeReference(getClassNameForNmNode(node.Parent));
                getParentMethod.Statements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(getParentMethod.ReturnType, new CodeExpression[] { })));
                class1.Members.Add(getParentMethod);

                compileUnit.Namespaces[0].Types.Add(class1);
            }
        }
        private void genAliasClasses()
        {
            genRootAliasesClass();
            foreach (var node in _doc.Descendants("A"))
            {
                string className = getClassNameForAliasNode(node);
                string fullAliasPath = getFullAliasPath(node);
                string guid = node.Attribute("guid").Value;
                string fullNmPath = getFullNmPathByGuid(guid);

                CodeTypeDeclaration class1 = new CodeTypeDeclaration(className);
                class1.Comments.Add(new CodeCommentStatement("Owner Guid" + guid));

                class1.IsClass = true;
                class1.BaseTypes.Add(new CodeTypeReference("UiAliasedNode"));

                //constructor
                CodeConstructor constructor = new CodeConstructor();
                constructor.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                constructor.BaseConstructorArgs.Add(new CodePrimitiveExpression(fullNmPath));
                constructor.BaseConstructorArgs.Add(new CodePrimitiveExpression(fullAliasPath));
                class1.Members.Add(constructor);

                //childs
                foreach (var child in node.Elements("A"))
                {
                    CodeMemberProperty ct = new CodeMemberProperty();
                    ct.Name = child.Attribute("name").Value;
                    ct.HasGet = true;
                    ct.HasSet = false;
                    ct.Type = new CodeTypeReference(getClassNameForAliasNode(child));
                    ct.Attributes = MemberAttributes.Public | MemberAttributes.Final;

                    ct.GetStatements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(ct.Type, new CodeExpression[] { })));

                    class1.Members.Add(ct);
                }
                //parent
                CodeMemberMethod getParentMethod = new CodeMemberMethod();
                getParentMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                getParentMethod.Name = "getParent";
                getParentMethod.ReturnType = new CodeTypeReference(getClassNameForAliasNode(node.Parent));
                getParentMethod.Statements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(getParentMethod.ReturnType, new CodeExpression[] { })));
                class1.Members.Add(getParentMethod);
                //toNameMapping
                string classNameForNmNode = getClassNameForNmNode(getNmNode(guid));
                if (classNameForNmNode != "")
                {
                    CodeMemberMethod getNmMethod = new CodeMemberMethod();
                    getNmMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                    getNmMethod.Name = "getNm";
                    getNmMethod.ReturnType = new CodeTypeReference(classNameForNmNode);
                    getNmMethod.Statements.Add(
                        new CodeMethodReturnStatement(new CodeObjectCreateExpression(getNmMethod.ReturnType,
                                                                                     new CodeExpression[] {})));
                    class1.Members.Add(getNmMethod);
                }
                else
                {
                    Console.WriteLine("Aliases without NameMapping: "+fullAliasPath);
                }

                compileUnit.Namespaces[0].Types.Add(class1);
            }
        }
        private void genRootAliasesClass()
        {
            string className = "Aliases";
            string fullAliasPath = "Aliases";
            string fullNmPath = "";

            var node = _doc.Descendants("Aliases").Single();

            CodeTypeDeclaration class1 = new CodeTypeDeclaration(className);

            class1.IsClass = true;
            class1.BaseTypes.Add(new CodeTypeReference("UiAliasedNode"));

            //constructor
            CodeConstructor constructor = new CodeConstructor();
            constructor.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            constructor.BaseConstructorArgs.Add(new CodePrimitiveExpression(fullNmPath));
            constructor.BaseConstructorArgs.Add(new CodePrimitiveExpression(fullAliasPath));
            class1.Members.Add(constructor);

            //childs
            foreach (var child in node.Elements("A"))
            {
                CodeMemberProperty ct = new CodeMemberProperty();
                ct.Name = child.Attribute("name").Value;
                ct.HasGet = true;
                ct.HasSet = false;
                ct.Type = new CodeTypeReference(getClassNameForAliasNode(child));
                ct.Attributes = MemberAttributes.Public | MemberAttributes.Final;

                ct.GetStatements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(ct.Type, new CodeExpression[] { })));

                class1.Members.Add(ct);
            }

            compileUnit.Namespaces[0].Types.Add(class1);
        }
        private void genRootNameMappingClass()
        {
            string fullNmPath = "NameMapping";
            string className = "NameMapping";
            var node = _doc.Descendants("NameMapping").Single();

            CodeTypeDeclaration class1 = new CodeTypeDeclaration(className);

            class1.IsClass = true;
            class1.BaseTypes.Add(new CodeTypeReference("UiMappedNode"));

            //constructor
            CodeConstructor constructor = new CodeConstructor();
            constructor.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            constructor.BaseConstructorArgs.Add(new CodePrimitiveExpression(fullNmPath));
            class1.Members.Add(constructor);

            //childs
            foreach (var child in node.Elements("NM"))
            {
                CodeMemberProperty ct = new CodeMemberProperty();
                ct.Name = child.Attribute("name").Value;
                ct.HasGet = true;
                ct.HasSet = false;
                ct.Type = new CodeTypeReference(getClassNameForNmNode(child));
                ct.Attributes = MemberAttributes.Public | MemberAttributes.Final;

                ct.GetStatements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(ct.Type, new CodeExpression[] { })));

                class1.Members.Add(ct);
            }

            compileUnit.Namespaces[0].Types.Add(class1);
        }
        private void genCoreClasses()
        {
            //class UiMappedNode
            genCoreUiMappedNodeClass();

            //class UiAliasedNode
            genCoreUiAlliasedNodeClass();
        }
        private void genCoreUiAlliasedNodeClass()
        {
            var uiAliasedNode = new CodeTypeDeclaration("UiAliasedNode");
            uiAliasedNode.IsClass = true;
            uiAliasedNode.Attributes = MemberAttributes.Private;
            uiAliasedNode.BaseTypes.Add(new CodeTypeReference("UiMappedNode"));

            //Fileds
            CodeMemberField aliasedNameField = new CodeMemberField();
            aliasedNameField.Attributes = MemberAttributes.Private; //todo: add readonly
            aliasedNameField.Name = "_aliasedName";
            aliasedNameField.Type = new CodeTypeReference(typeof(System.String));
            uiAliasedNode.Members.Add(aliasedNameField);

            //Constructor
            CodeConstructor constructor = new CodeConstructor();
            constructor.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(System.String), "fullName"));
            constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(System.String), "aliasedName"));
            constructor.BaseConstructorArgs.Add(new CodeVariableReferenceExpression("fullName"));

            //... _aliasedName = aliasedName
            CodeFieldReferenceExpression clFullName = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_aliasedName");
            constructor.Statements.Add(new CodeAssignStatement(clFullName, new CodeArgumentReferenceExpression("aliasedName")));
            uiAliasedNode.Members.Add(constructor);

            //Properties
            CodeMemberProperty ct;
            ct = new CodeMemberProperty();
            ct.Name = "nAliasName";
            ct.HasGet = true;
            ct.HasSet = false;
            ct.Type = new CodeTypeReference(typeof(System.String));
            ct.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            ct.GetStatements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_aliasedName")));
            uiAliasedNode.Members.Add(ct);

            ct = new CodeMemberProperty();
            ct.Name = "nName";
            ct.HasGet = true;
            ct.HasSet = false;
            ct.Type = new CodeTypeReference(typeof(System.String));
            ct.Attributes = MemberAttributes.Public | MemberAttributes.Override;
            var splitInvoke = new CodeMethodInvokeExpression(new CodeVariableReferenceExpression("nAliasName"),
                                                             "Split", new CodeExpression[] { new CodePrimitiveExpression('.') });
            var lastInvoke = new CodeMethodInvokeExpression(splitInvoke,
                                                            "Last", new CodeExpression[] { });


            ct.GetStatements.Add(new CodeMethodReturnStatement(lastInvoke));
            uiAliasedNode.Members.Add(ct);

            compileUnit.Namespaces[0].Types.Add(uiAliasedNode);
        }
        private void genCoreUiMappedNodeClass()
        {
            var uiMappedNode = new CodeTypeDeclaration("UiMappedNode");
            uiMappedNode.IsClass = true;
            uiMappedNode.Attributes = MemberAttributes.Private | MemberAttributes.Abstract;

            //Fileds
            CodeMemberField fullNameField = new CodeMemberField();
            fullNameField.Attributes = MemberAttributes.Private; //todo: add readonly
            fullNameField.Name = "_fullName";
            fullNameField.Type = new CodeTypeReference(typeof(System.String));
            uiMappedNode.Members.Add(fullNameField);

            //Constructor
            CodeConstructor constructor = new CodeConstructor();
            constructor.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(System.String), "fullName"));

            //... _fullName = fullName
            CodeFieldReferenceExpression clFullName = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_fullName");
            constructor.Statements.Add(new CodeAssignStatement(clFullName, new CodeArgumentReferenceExpression("fullName")));
            uiMappedNode.Members.Add(constructor);

            //Properties
            CodeMemberProperty ct;
            ct = new CodeMemberProperty();
            ct.Name = "nFullName";
            ct.HasGet = true;
            ct.HasSet = false;
            ct.Type = new CodeTypeReference(typeof(System.String));
            ct.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            ct.GetStatements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_fullName")));
            uiMappedNode.Members.Add(ct);

            ct = new CodeMemberProperty();
            ct.Name = "nName";
            ct.HasGet = true;
            ct.HasSet = false;
            ct.Type = new CodeTypeReference(typeof(System.String));
            ct.Attributes = MemberAttributes.Public; //public virtual
            var splitInvoke = new CodeMethodInvokeExpression(new CodeVariableReferenceExpression("nFullName"),
                                                             "Split", new CodeExpression[] {new CodePrimitiveExpression('.')});
            var lastInvoke = new CodeMethodInvokeExpression(splitInvoke, 
                                                            "Last", new CodeExpression[]{});


            ct.GetStatements.Add(new CodeMethodReturnStatement(lastInvoke));
            uiMappedNode.Members.Add(ct);

            compileUnit.Namespaces[0].Types.Add(uiMappedNode);
        }
        #endregion

        #region Helpers
        private string getFullNmPath(XElement node)
        {
            return getFullPath(node, "NM", "NameMapping"); //todo: move consts
        }
        private string getFullAliasPath(XElement node)
        {
            return getFullPath(node, "A", "Aliases"); //todo: move consts
        }
        private string getFullPath(XElement node, string nodeName, string rootName)
        {
            if (node.Attribute("name") == null)
            {
                return rootName;
            }

            string str = node.Attribute("name").Value;
            XElement rec = node;
            while (rec.Parent.Name == nodeName)
            {
                rec = rec.Parent;
                str = rec.Attribute("name").Value + "." + str;
            }
            return rootName + "." + str;
        }
        private string getClassNameForNmNode (XElement node)
        {
            if (node == null)
            {
                return "";
            }
            if (node.Attribute("name") == null)
            {
                return "NameMapping";
            }

            string baseName;
            baseName = node.Attribute("name").Value + "_";
            baseName += node.Attribute("guid").Value.Trim(new char[] {'{', '}'}).Replace('-','_');
            return baseName;
        }
        private string getClassNameForAliasNode(XElement node)
        {
            if (node.Attribute("name") == null)
            {
                return "Aliases";
            }

            string fullPath = getFullAliasPath(node);
            if (classDictionary.ContainsKey(fullPath))
            {
                return classDictionary[fullPath];
            }

            string baseName;
            baseName = "Al_" + node.Attribute("name").Value + "_";
            baseName += node.Attribute("guid").Value.Trim(new char[] {'{', '}'}).Replace('-', '_');

            string indexedName = baseName;
            int i = 2;
            while (classDictionary.ContainsValue(indexedName))
            {
                indexedName = baseName + "_" + i;
                i++;
            }
            classDictionary.Add(fullPath, indexedName);

            return indexedName;
        }
        private string getFullNmPathByGuid(string guid)
        {
            var owner = getNmNode(guid);
            if (owner == null)
            {
                return "";
            }
            return getFullNmPath(owner);
        }
        private XElement getNmNode(string guid)
        {
            return _doc.Descendants("NM").Where(node => node.Attribute("guid").Value == guid).SingleOrDefault();
        }
        #endregion
    }
}
